﻿using System;
using System.Collections.Generic;
using System.Linq;
using FitNesseRunner.Model;

namespace FitNesseRunner
{
	internal class ScriptTable : ExecutableTable
	{
		public ScriptTable(IEnumerable<IEnumerable<string>> cells, IReporter reporter, IContext context) 
			: base(cells, 1, context.ReflectionProvider)
		{
			_variables = context.Variables;
			_reporter = reporter;
			_context = context;
			_className = base.ClassName;

			InitializeKeywordHandlers();
		}

		private void InitializeKeywordHandlers()
		{
			_keywordToHandler = new Dictionary<string, Action<object, IEnumerable<string>>>
				{
					{"check", HandleCheckKeyword},
					{"ensure", HandleEnsureKeyword},
					{"show", HandleShowKeyword},
					{"reject", HandleRejectKeyword }
				};
			return;
		}

		private void HandleRejectKeyword(object instance, IEnumerable<string> row)
		{
			var methodName = ToPascalCase(row.ElementAt(1));
			var result = Convert.ToBoolean(InvokeRow(instance, methodName, row.Skip(2)));
			if (result)
				_reporter.Failed(instance, methodName, false, true);
			else
				_reporter.Pass(methodName + " is false");
		}

		private readonly string _className;
		private readonly IReporter _reporter;
		private readonly IContext _context;
		private readonly IDictionary<string, string> _variables;
		private Dictionary<string, Action<object, IEnumerable<string>>> _keywordToHandler;

		protected override string ClassName
		{
			get { return _className; }
		}

		public override void ExecuteAll()
		{
			object instance;
			if (_className == null)
			{
				instance = _context.LastScriptTableObject;
				if (instance == null)
				{
					_reporter.TestError("Script table doesn't contain a class name");
					return;
				}
			}
			else
			{
				try
				{
					instance = CreateInstance();
					_context.LastScriptTableObject = instance;
					_reporter.Constructing(ClassName);
				}
				catch (Exception e)
				{
					_reporter.FailedToConstruct(_className, e);
					return;
				}
			}

			foreach (var row in Cells.Skip(1))
			{
				ExecuteRow(row, instance);
			}
		}

		private void ExecuteRow(IEnumerable<string> row, object instance)
		{
			var methodName = ToPascalCase(row.First());
			if (IsKeyword(methodName))
			{
				HandleKeyword(instance, methodName, row);
				return;
			}

			if (IsVariableAssignment(methodName))
			{
				var variableName = GetVariableName(methodName);
				row = row.Skip(1);
				methodName = ToPascalCase(row.First());
				var variableValue = SafeInvokeRow(row, methodName, instance);
				_variables[variableName] = variableValue;
				return;
			}

			SafeInvokeRow(row, methodName, instance);
		}

		private static string GetVariableName(string cellContent)
		{
			return cellContent.Substring(1, cellContent.Length - 2);
		}

		private static bool IsVariableAssignment(string cellValue)
		{
			return cellValue.StartsWith("$") && cellValue.EndsWith("=");
		}

		private string SafeInvokeRow(IEnumerable<string> row, string methodName, object instance)
		{
			var parameterCells = row.Skip(1);
			_reporter.Invoking(ClassName, methodName, parameterCells);
			try
			{
				return InvokeRow(instance, methodName, parameterCells);
			}
			catch (Exception e)
			{
				_reporter.InvocationFailed(ClassName, methodName, parameterCells, e);
				return null;
			}
		}

		private string InvokeRow(object instance, string methodName, IEnumerable<string> parameterCells)
		{
			methodName = methodName.TrimStart('$');
			var parameters = parameterCells.Every(2).Select(o => o.Trim());
			parameters = ResolveVariables(parameters);
			methodName += string.Concat(parameterCells.Skip(1).Every(2)).Replace(" ", string.Empty);
			return Convert.ToString(ReflectionProvider.Invoke(instance, methodName, parameters));
		}

		private IEnumerable<string> ResolveVariables(IEnumerable<string> parameters)
		{
			var resolver = new VariablesResolver(_variables);
			foreach (var parameter in parameters)
			{
				yield return resolver.Resolve(parameter);
			}
		}

		private void HandleKeyword(object instance, string keyword, IEnumerable<string> row)
		{
			Action<object, IEnumerable<string>> handler;
			if (!_keywordToHandler.TryGetValue(keyword.ToLower(), out handler))
				throw new NotImplementedException();

			handler(instance, row);
		}

		private void HandleShowKeyword(object instance, IEnumerable<string> row)
		{
			var methodName = ToPascalCase(row.ElementAt(1));
			var result = InvokeRow(instance, methodName, row.Skip(2));

			_reporter.Show(methodName, result);
		}

		private void HandleEnsureKeyword(object instance, IEnumerable<string> row)
		{
			var methodName = ToPascalCase(row.ElementAt(1));
			var result = InvokeRow(instance, methodName, row.Skip(2));

			if (Convert.ToBoolean(result))
				_reporter.Pass("{0} is true", methodName)
				;
			else
				_reporter.Failed(instance, methodName, true, false);
		}

		private void HandleCheckKeyword(object instance, IEnumerable<string> row)
		{
			var methodName = ToPascalCase(row.ElementAt(1));
			var result = InvokeRow(instance, methodName, row.Skip(2).Take(row.Count() - 3));

			var expectedValue = row.Last().Trim();
			if (result == expectedValue)
			{
				_reporter.Pass("{0}=={1}", methodName, expectedValue);
			}
			else
			{
				_reporter.Failed("Check failed. {0}=={1} but expected {2}", methodName, expectedValue, result);
			}
		}

		private bool IsKeyword(string methodName)
		{
			return _keywordToHandler.Keys.Contains(methodName.ToLower());
		}
	}
}